Skip to content

fix: replace underpriced dapp-suggested and saved gas fees with suggested estimates - #9704

Merged
cloudonshore merged 11 commits into
mainfrom
fix/underpriced-dapp-gas-fees
Aug 4, 2026
Merged

fix: replace underpriced dapp-suggested and saved gas fees with suggested estimates#9704
cloudonshore merged 11 commits into
mainfrom
fix/underpriced-dapp-gas-fees

Conversation

@cloudonshore

@cloudonshore cloudonshore commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Explanation

Underpriced gas fees — a maxFeePerGas below (or barely above) the current base fee — produce transactions that are very unlikely to be included before fee conditions change further. They typically end up stuck as pending until eventually marked as failed (e.g. Transaction not found on network after timeout), with no actionable feedback to the user. Telemetry shows this is a meaningful contributor to dropped/timed-out transactions.

Two fee sources can produce such values, and nothing in the client validates either against current network conditions. This PR adds a feature-flag-gated staleness gate for both, at transaction creation time — the last point where fees can still be changed before signing.

1. Dapp-suggested fees (replaceUnderpricedDappGasFees flag)

Currently:

  • updateGasFees uses initialParams.maxFeePerGas as-is.
  • Gas estimation deliberately strips fee fields before calling the RPC node, so the node's own fee-cap validation never runs.
  • GasFeePoller only refreshes fees for preset levels, so dappSuggested fees stay frozen while the confirmation is open.

A dapp that computes fees from stale data can therefore produce a signed transaction that cannot mine. With the flag enabled:

  • A new shouldIgnoreDappGasFees check fires when the dapp-suggested maxFeePerGas (or a legacy gasPrice used as maxFeePerGas on EIP-1559 networks) is below the current low estimate. The low estimate is the floor rather than the raw base fee, since fees between the base fee and the low estimate have near-zero effective priority fee and no headroom for base fee movement, so they share the same failure mode.
  • When it fires, the transaction falls through to the existing suggested (medium) fee logic for both maxFeePerGas and maxPriorityFeePerGas.
  • userFeeLevel is set to medium instead of dappSuggested, which also opts the transaction into GasFeePoller refresh while unapproved.
  • The original values remain available via dappSuggestedGasFees on the transaction metadata, so UIs can still display the site suggestion and users can revert via the edit modal.

Note: for transactions with complete dapp-suggested fees, getSuggestedGasFees previously skipped fetching estimates entirely. When the flag is enabled, estimates are now fetched for these transactions so the comparison can run — one additional gas fee estimate call per dapp transaction on enabled chains.

2. Saved (advanced) gas fee preferences (replaceUnderpricedSavedGasFees flag)

A saved custom preference is a static maxBaseFee captured at some point in the past; once the base fee rises past it, every transaction priced from it strands. Nothing else guards this source: the dapp gate deliberately excludes savedGasFees, and hasInitialGasFeeParams only protects transactions carrying explicit fee params — saved fees apply precisely when they don't. This is increasingly relevant as clients expand saved-preference usage (per-account custom gas settings persisted from the confirmation flow, and #9682 extending saved fees to wallet-initiated transfers).

With the flag enabled:

  • After suggested fees are fetched, if the saved preference has a custom maxBaseFee below the current low estimate, the saved preference is ignored for this transaction and the suggested (medium) values are used instead.
  • Level-based saved preferences (low/medium/high) are never touched — they track current estimates by construction.
  • userFeeLevel becomes medium instead of custom, with the same GasFeePoller refresh benefit.
  • No fetch-path changes needed: transactions eligible for saved fees carry no fee params, so estimates are already fetched.

Guards / fail-open behavior (both gates)

Fees are kept unchanged when: the respective feature flag is not enabled for the chain (both default to disabled; per-chain rollout via perChainConfig with default fallback, mirroring timeoutAttempts); the transaction is internal (dapp gate; swaps/bridges are priced by the aggregator/relay); the fee is at or above the low estimate; or estimates are unavailable / not fee-market type.

The product decision (saved-fees gate)

Overriding a value the user saved is a stronger intervention than overriding a dapp's computed value. Three defensible behaviors when the saved-fees gate fires: (1) silently use suggested fees (this PR's behavior), (2) use suggested fees + surface an alert ("your saved gas setting is below current network fees"), (3) keep the saved value + warn only. The flag ships disabled, so this can be settled before enablement.

An argument for intervening (1 or 2) rather than deferring entirely to the saved value: saved preferences already do not grant full autonomy. The controller already ignores them for swaps and bridges (SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES, #9401/#9682) — explicitly because a saved fee could underprice aggregator-priced transactions — and already yields them to explicit fee params (hasInitialGasFeeParams, #8993). The established principle is that saved preferences apply where they can work and yield where they would predictably break the transaction. A custom maxBaseFee below the current low estimate is the clearest instance of the latter — this extends existing precedent rather than setting new one.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Changes pre-sign gas selection for dapp and saved-fee paths when flags are enabled; behavior is gated and fail-open, but wrong thresholds or rollout could surprise users or sites.

Overview
Adds opt-in, per-chain gates in updateGasFees so transactions are less likely to be priced below current network conditions before approval.

When replaceUnderpricedDappGasFees is enabled, dapp-suggested EIP-1559 fees whose effective maxFeePerGas (including legacy gasPrice used as max fee) is below the wallet’s low estimate are replaced with suggested medium maxFeePerGas / maxPriorityFeePerGas. userFeeLevel becomes medium instead of dappSuggested, so fees can keep updating while unapproved; originals stay on dappSuggestedGasFees. Complete dapp fee params no longer skip estimate fetch when this flag is on (needed for the comparison; fail-open if estimates are missing).

When replaceUnderpricedSavedGasFees is enabled, saved custom maxBaseFee below the low estimate is dropped for that tx so medium suggestions apply; level-based saved prefs are unchanged. userFeeLevel becomes medium instead of custom.

New remote feature-flag helpers and types mirror existing per-chain default / perChainConfig patterns; both flags default to disabled.

Reviewed by Cursor Bugbot for commit 95e622d. Bugbot is set up for automated code reviews on this repo. Configure here.

@cloudonshore
cloudonshore requested review from a team as code owners July 30, 2026 06:42
@cloudonshore

Copy link
Copy Markdown
Contributor Author

Known edge case: dapp max-spend calculations

If a dapp computes a maximum spendable amount from its own fee suggestion (e.g. value = balance - gas × dappMaxFeePerGas for a native max send/wrap/bridge), replacing an underpriced fee with the medium estimate can push the worst-case reservation (value + gas × maxFeePerGas) above the account balance, surfacing an insufficient-funds state for a transaction the dapp's UI presented as affordable.

Worth noting: for the gate to fire, the base fee has already risen past the dapp's data — so the dapp's amount is stale too, not just its fee. There is no fee assignment that both fits the balance and can currently be included: the affordable (dapp) fee can't mine, and any mineable fee breaks the max-spend arithmetic. The replacement converts a silent, un-actionable failure (stuck pending, nonce occupied) into an explicit one, but it can still contradict the dapp's arithmetic in a user-visible way.

Proposed refinement (follow-up): clamp the replacement to what the balance can bear, since the actual charge (baseFee + tip) is far below the reservation:

replacementMaxFee = min(medium, (balance − value) / gas)

and only proceed if the result is still ≥ the low estimate. If it is, the transaction fits the balance and can mine — the max-spend flow works unchanged. If it isn't, the amount is genuinely unaffordable at any viable fee, and the replacement is skipped (fail open, current behavior). Cost is one balance read, only when the gate fires. The clamp should also be skipped for sponsored-gas / gas-fee-token transactions, where maxFeePerGas never draws on the native balance (those are safe to bump freely).

Suggested alongside: break down the eventual metrics by outcome (replaced / replaced_clamped / skipped_unaffordable) so the real-world rate of this edge case is measurable.

@cloudonshore cloudonshore changed the title fix: replace underpriced dapp-suggested gas fees with suggested estimates fix: replace underpriced dapp-suggested and saved gas fees with suggested estimates Jul 30, 2026
Comment thread packages/transaction-controller/src/utils/gas-fees.test.ts Outdated
Comment thread packages/transaction-controller/src/utils/gas-fees.ts

@pedronfigueiredo pedronfigueiredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good just two small nits

@cloudonshore
cloudonshore added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
@cloudonshore
cloudonshore added this pull request to the merge queue Aug 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 3, 2026
@cloudonshore
cloudonshore added this pull request to the merge queue Aug 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 3, 2026
@cloudonshore
cloudonshore added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 4b8f3ad Aug 4, 2026
136 checks passed
@cloudonshore
cloudonshore deleted the fix/underpriced-dapp-gas-fees branch August 4, 2026 08:32
@cursor cursor Bot mentioned this pull request Aug 4, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants